/*
 * @(#)CookieModule.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ProtocolException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * This module handles Netscape cookies (also called Version 0 cookies) and
 * Version 1 cookies. Specifically is reads the <var>Set-Cookie</var> and
 * <var>Set-Cookie2</var> response headers and sets the <var>Cookie</var> and
 * <var>Cookie2</var> headers as neccessary.
 * <p/>
 * The accepting and sending of cookies is controlled by a
 * <var>CookiePolicyHandler</var>. This allows you to fine tune your privacy
 * preferences. A cookie is only added to the cookie list if the handler allows
 * it, and a cookie from the cookie list is only sent if the handler allows it.
 * <p/>
 * This module expects to be the only one handling cookies. Specifically, it
 * will remove any <var>Cookie</var> and <var>Cookie2</var> header fields found
 * in the request, and it will remove the <var>Set-Cookie</var> and
 * <var>Set-Cookie2</var> header fields in the response (after processing them).
 * In order to add cookies to a request or to prevent cookies from being sent,
 * you can use the {@link #addCookie(HTTPClient.Cookie) addCookie} and
 * {@link #removeCookie(HTTPClient.Cookie) removeCookie} methods to manipulate
 * the module's list of cookies.
 * <p/>
 * A cookie jar can be used to store cookies between sessions. This file is read
 * when this class is loaded and is written when the application exits; only
 * cookies from the default context are saved. The name of the file is
 * controlled by the system property <var>HTTPClient.cookies.jar</var> and
 * defaults to a system dependent name. The reading and saving of cookies is
 * enabled by setting the system property <var>HTTPClient.cookies.save</var> to
 * <var>true</var>.
 *
 * @author Ronald Tschal�r
 * @version 0.3-3 06/05/2001
 * @see <a
 *      href="http://home.netscape.com/newsref/std/cookie_spec.html">Netscape's
 *      cookie spec</a>
 * @see <a href="http://www.ietf.org/rfc/rfc2965.txt">HTTP State Management *
 *      Mechanism spec< /a>
 * @since V0.3
 */
@SuppressWarnings("deprecation")
public class CookieModule implements HTTPClientModule {
    /** the list of known cookies */
    private static Hashtable cookie_cntxt_list = new Hashtable();

    /** the file to use for persistent cookie storage */
    private static File cookie_jar = null;

    /** an object, whose finalizer will save the cookies to the jar */
    private static Object cookieSaver = null;

    /** the cookie policy handler */
    private static CookiePolicyHandler cookie_handler = new DefaultCookiePolicyHandler();

    private static final Logger log = Logger.getLogger(CookieModule.class);

    // read in cookies from disk at startup

    static {
        boolean persist;
        try {
            persist = Boolean.getBoolean("HTTPClient.cookies.save");
        } catch (Exception e) {
            persist = false;
        }

        if (persist) {
            loadCookies();

            // the nearest thing to atexit() I know of...

            cookieSaver = new Object() {
                public void finalize() {
                    saveCookies();
                }
            };
            try {
                System.runFinalizersOnExit(true);
            } catch (Throwable t) {
            }
        }
    }

    private static void loadCookies() {
        // The isFile() etc need to be protected by the catch as signed
        // applets may be allowed to read properties but not do IO
        try {
            cookie_jar = new File(getCookieJarName());
            if (cookie_jar.isFile() && cookie_jar.canRead()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(cookie_jar));
                cookie_cntxt_list.put(HTTPConnection.getDefaultContext(), ois.readObject());
                ois.close();
            }
        } catch (Throwable t) {
            cookie_jar = null;
        }
    }

    private static void saveCookies() {
        if (cookie_jar != null && (!cookie_jar.exists() || cookie_jar.isFile() && cookie_jar.canWrite())) {
            Hashtable cookie_list = new Hashtable();
            Enumeration en = Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext()).elements();

            // discard cookies which are not to be kept across sessions

            while (en.hasMoreElements()) {
                Cookie cookie = (Cookie)en.nextElement();
                if (!cookie.discard())
                    cookie_list.put(cookie, cookie);
            }

            // save any remaining cookies in jar
            if (cookie_list.size() > 0) {
                try {
                    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(cookie_jar));
                    oos.writeObject(cookie_list);
                    oos.close();
                } catch (Throwable t) {
                }
            }
        }
    }

    private static String getCookieJarName() {
        String file = null;

        try {
            file = System.getProperty("HTTPClient.cookies.jar");
        } catch (Exception e) {
        }

        if (file == null) {
            // default to something reasonable

            String os = System.getProperty("os.name");
            if (os.equalsIgnoreCase("Windows 95") || os.equalsIgnoreCase("16-bit Windows")
                || os.equalsIgnoreCase("Windows")) {
                file = System.getProperty("java.home") + File.separator + ".httpclient_cookies";
            } else if (os.equalsIgnoreCase("Windows NT")) {
                file = System.getProperty("user.home") + File.separator + ".httpclient_cookies";
            } else if (os.equalsIgnoreCase("OS/2")) {
                file = System.getProperty("user.home") + File.separator + ".httpclient_cookies";
            } else if (os.equalsIgnoreCase("Mac OS") || os.equalsIgnoreCase("MacOS")) {
                file = "System Folder" + File.separator + "Preferences" + File.separator + "HTTPClientCookies";
            } else
            // it's probably U*IX or VMS
            {
                file = System.getProperty("user.home") + File.separator + ".httpclient_cookies";
            }
        }

        return file;
    }

    // Constructors

    CookieModule() {
    }

    // Methods

    /** Invoked by the HTTPClient. */
    public int requestHandler(Request req, Response[] resp) {
        // First remove any Cookie headers we might have set for a previous
        // request

        NVPair[] hdrs = req.getHeaders();
        int length = hdrs.length;
        for (int idx = 0; idx < hdrs.length; idx++) {
            int beg = idx;
            while (idx < hdrs.length && hdrs[idx].getName().equalsIgnoreCase("Cookie"))
                idx++;

            if (idx - beg > 0) {
                length -= idx - beg;
                System.arraycopy(hdrs, idx, hdrs, beg, length - beg);
            }
        }
        if (length < hdrs.length) {
            hdrs = Util.resizeArray(hdrs, length);
            req.setHeaders(hdrs);
        }

        // Now set any new cookie headers

        Hashtable cookie_list = Util.getList(cookie_cntxt_list, req.getConnection().getContext());
        if (cookie_list.size() == 0)
            return REQ_CONTINUE; // no need to create a lot of objects

        Vector names = new Vector();
        Vector lens = new Vector();
        int version = 0;

        synchronized (cookie_list) {
            Enumeration list = cookie_list.elements();
            Vector remove_list = null;

            while (list.hasMoreElements()) {
                Cookie cookie = (Cookie)list.nextElement();

                if (cookie.hasExpired()) {
                    if (log.isDebugEnabled())
                        log.debug("Cookie has expired and is being removed: " + cookie);

                    if (remove_list == null)
                        remove_list = new Vector();
                    remove_list.addElement(cookie);
                    continue;
                }

                if (cookie.sendWith(req) && (cookie_handler == null || cookie_handler.sendCookie(cookie, req))) {
                    int len = cookie.getPath().length();
                    int idx;

                    // insert in correct position
                    for (idx = 0; idx < lens.size(); idx++)
                        if (((Integer)lens.elementAt(idx)).intValue() < len)
                            break;

                    names.insertElementAt(cookie.toExternalForm(), idx);
                    lens.insertElementAt(new Integer(len), idx);

                    if (cookie instanceof Cookie2)
                        version = Math.max(version, ((Cookie2)cookie).getVersion());
                }
            }

            // remove any marked cookies
            // Note: we can't do this during the enumeration!
            if (remove_list != null) {
                for (int idx = 0; idx < remove_list.size(); idx++)
                    cookie_list.remove(remove_list.elementAt(idx));
            }
        }

        if (!names.isEmpty()) {
            StringBuffer value = new StringBuffer();

            if (version > 0)
                value.append("$Version=\"" + version + "\"; ");

            value.append((String)names.elementAt(0));
            for (int idx = 1; idx < names.size(); idx++) {
                value.append("; ");
                value.append((String)names.elementAt(idx));
            }
            hdrs = Util.resizeArray(hdrs, hdrs.length + 1);
            hdrs[hdrs.length - 1] = new NVPair("Cookie", value.toString());

            // add Cookie2 header if necessary
            if (version != 1) // we currently know about version 1 only
            {
                int idx;
                for (idx = 0; idx < hdrs.length; idx++)
                    if (hdrs[idx].getName().equalsIgnoreCase("Cookie2"))
                        break;
                if (idx == hdrs.length) {
                    hdrs = Util.resizeArray(hdrs, hdrs.length + 1);
                    hdrs[hdrs.length - 1] = new NVPair("Cookie2", "$Version=\"1\"");
                }
            }

            req.setHeaders(hdrs);

            if (log.isDebugEnabled())
                log.debug("Sending cookies '" + value + "'");

        }

        return REQ_CONTINUE;
    }

    /** Invoked by the HTTPClient. */
    public void responsePhase1Handler(Response resp, RoRequest req) throws IOException {
        String set_cookie = resp.getHeader("Set-Cookie");
        String set_cookie2 = resp.getHeader("Set-Cookie2");
        if (set_cookie == null && set_cookie2 == null)
            return;

        resp.deleteHeader("Set-Cookie");
        resp.deleteHeader("Set-Cookie2");

        if (set_cookie != null)
            handleCookie(set_cookie, false, req, resp);
        if (set_cookie2 != null)
            handleCookie(set_cookie2, true, req, resp);
    }

    /** Invoked by the HTTPClient. */
    public int responsePhase2Handler(Response resp, Request req) {
        return RSP_CONTINUE;
    }

    /** Invoked by the HTTPClient. */
    public void responsePhase3Handler(Response resp, RoRequest req) {
    }

    /** Invoked by the HTTPClient. */
    public void trailerHandler(Response resp, RoRequest req) throws IOException {
        String set_cookie = resp.getTrailer("Set-Cookie");
        String set_cookie2 = resp.getTrailer("Set-Cookie2");
        if (set_cookie == null && set_cookie2 == null)
            return;

        resp.deleteTrailer("Set-Cookie");
        resp.deleteTrailer("Set-Cookie2");

        if (set_cookie != null)
            handleCookie(set_cookie, false, req, resp);
        if (set_cookie2 != null)
            handleCookie(set_cookie2, true, req, resp);
    }

    private void handleCookie(String set_cookie, boolean cookie2, RoRequest req, Response resp) throws ProtocolException {
        Cookie[] cookies;
        if (cookie2)
            cookies = Cookie2.parse(set_cookie, req);
        else
            cookies = Cookie.parse(set_cookie, req);

        if (log.isDebugEnabled()) {
            log.debug("Received and parsed " + cookies.length + " cookies:");
            for (int idx = 0; idx < cookies.length; idx++)
                log.debug("Cookie " + idx + ": " + cookies[idx]);
        }

        Hashtable cookie_list = Util.getList(cookie_cntxt_list, req.getConnection().getContext());
        synchronized (cookie_list) {
            for (int idx = 0; idx < cookies.length; idx++) {
                Cookie cookie = (Cookie)cookie_list.get(cookies[idx]);
                if (cookie != null && cookies[idx].hasExpired()) {
                    if (log.isDebugEnabled())
                        log.debug("Cookie has expired and is " + "being removed: " + cookie);

                    cookie_list.remove(cookie); // expired, so remove
                } else if (!cookies[idx].hasExpired()) // new or replaced
                {
                    if (cookie_handler == null || cookie_handler.acceptCookie(cookies[idx], req, resp))
                        cookie_list.put(cookies[idx], cookies[idx]);
                }
            }
        }
    }

    /**
     * Discard all cookies for all contexts. Cookies stored in persistent storage
     * are not affected.
     */
    public static void discardAllCookies() {
        cookie_cntxt_list.clear();
    }

    /**
     * Discard all cookies for the given context. Cookies stored in persistent
     * storage are not affected.
     *
     * @param context
     *         the context Object
     */
    public static void discardAllCookies(Object context) {
        if (context != null)
            cookie_cntxt_list.remove(context);
    }

    /**
     * List all stored cookies for all contexts.
     *
     * @return an array of all Cookies
     * @since V0.3-1
     */
    public static Cookie[] listAllCookies() {
        synchronized (cookie_cntxt_list) {
            Cookie[] cookies = new Cookie[0];
            int idx = 0;

            Enumeration cntxt_list = cookie_cntxt_list.elements();
            while (cntxt_list.hasMoreElements()) {
                Hashtable cntxt = (Hashtable)cntxt_list.nextElement();
                synchronized (cntxt) {
                    cookies = Util.resizeArray(cookies, idx + cntxt.size());
                    Enumeration cookie_list = cntxt.elements();
                    while (cookie_list.hasMoreElements())
                        cookies[idx++] = (Cookie)cookie_list.nextElement();
                }
            }

            return cookies;
        }
    }

    /**
     * List all stored cookies for a given context.
     *
     * @param context
     *         the context Object.
     * @return an array of Cookies
     * @since V0.3-1
     */
    public static Cookie[] listAllCookies(Object context) {
        Hashtable cookie_list = Util.getList(cookie_cntxt_list, context);

        synchronized (cookie_list) {
            Cookie[] cookies = new Cookie[cookie_list.size()];
            int idx = 0;

            Enumeration en = cookie_list.elements();
            while (en.hasMoreElements())
                cookies[idx++] = (Cookie)en.nextElement();

            return cookies;
        }
    }

    /**
     * Add the specified cookie to the list of cookies in the default context. If
     * a compatible cookie (as defined by <var>Cookie.equals()</var>) already
     * exists in the list then it is replaced with the new cookie.
     *
     * @param cookie
     *         the Cookie to add
     * @since V0.3-1
     */
    public static void addCookie(Cookie cookie) {
        Hashtable cookie_list = Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext());
        cookie_list.put(cookie, cookie);
    }

    /**
     * Add the specified cookie to the list of cookies for the specified context.
     * If a compatible cookie (as defined by <var>Cookie.equals()</var>) already
     * exists in the list then it is replaced with the new cookie.
     *
     * @param cookie
     *         the cookie to add
     * @param context
     *         the context Object.
     * @since V0.3-1
     */
    public static void addCookie(Cookie cookie, Object context) {
        Hashtable cookie_list = Util.getList(cookie_cntxt_list, context);
        cookie_list.put(cookie, cookie);
    }

    /**
     * Remove the specified cookie from the list of cookies in the default
     * context. If the cookie is not found in the list then this method does
     * nothing.
     *
     * @param cookie
     *         the Cookie to remove
     * @since V0.3-1
     */
    public static void removeCookie(Cookie cookie) {
        Hashtable cookie_list = Util.getList(cookie_cntxt_list, HTTPConnection.getDefaultContext());
        cookie_list.remove(cookie);
    }

    /**
     * Remove the specified cookie from the list of cookies for the specified
     * context. If the cookie is not found in the list then this method does
     * nothing.
     *
     * @param cookie
     *         the cookie to remove
     * @param context
     *         the context Object
     * @since V0.3-1
     */
    public static void removeCookie(Cookie cookie, Object context) {
        Hashtable cookie_list = Util.getList(cookie_cntxt_list, context);
        cookie_list.remove(cookie);
    }

    /**
     * Sets a new cookie policy handler. This handler will be called for each
     * cookie that a server wishes to set and for each cookie that this module
     * wishes to send with a request. In either case the handler may allow or
     * reject the operation. If you wish to blindly accept and send all cookies
     * then just disable the handler with
     * <code>CookieModule.setCookiePolicyHandler(null);</code>.
     * <p/>
     * At initialization time a default handler is installed. This handler allows
     * all cookies to be sent. For any cookie that a server wishes to be set two
     * lists are consulted. If the server matches any host or domain in the
     * reject list then the cookie is rejected; if the server matches any host or
     * domain in the accept list then the cookie is accepted (in that order). If
     * no host or domain match is found in either of these two lists and user
     * interaction is allowed then a dialog box is poped up to ask the user
     * whether to accept or reject the cookie; if user interaction is not allowed
     * the cookie is accepted.
     * <p/>
     * The accept and reject lists in the default handler are initialized at
     * startup from the two properties <var>HTTPClient.cookies.hosts.accept</var>
     * and <var>HTTPClient.cookies.hosts.reject</var>. These properties must
     * contain a "|" separated list of host and domain names. All names beginning
     * with a "." are treated as domain names, all others as host names. An empty
     * string will match all hosts. The two lists are further expanded if the
     * user chooses one of the "Accept All from Domain" or
     * "Reject All from Domain" buttons in the dialog box.
     * <p/>
     * Note: the default handler does not implement the rules concerning
     * unverifiable transactions (section 3.3.6, <A
     * HREF="http://www.ietf.org/rfc/rfc2965.txt">RFC-2965</A>). The reason for
     * this is simple: the default handler knows nothing about the application
     * using this client, and it therefore does not have enough information to
     * determine when a request is verifiable and when not. You are therefore
     * encouraged to provide your own handler which implements section 3.3.6 (use
     * the <code>CookiePolicyHandler.sendCookie</code> method for this).
     *
     * @param handler
     *         the new policy handler
     * @return the previous policy handler
     */
    public static synchronized CookiePolicyHandler setCookiePolicyHandler(CookiePolicyHandler handler) {
        CookiePolicyHandler old = cookie_handler;
        cookie_handler = handler;
        return old;
    }
}

/** A simple cookie policy handler. */
class DefaultCookiePolicyHandler implements CookiePolicyHandler {
    /** a list of all hosts and domains from which to silently accept cookies */
    private String[] accept_domains = new String[0];

    /** a list of all hosts and domains from which to silently reject cookies */
    private String[] reject_domains = new String[0];

    /** the query popup */
    private BasicCookieBox popup = null;

    DefaultCookiePolicyHandler() {
        // have all cookies been accepted or rejected?
        String list;

        try {
            list = System.getProperty("HTTPClient.cookies.hosts.accept");
        } catch (Exception e) {
            list = null;
        }
        String[] domains = Util.splitProperty(list);
        for (int idx = 0; idx < domains.length; idx++)
            addAcceptDomain(domains[idx].toLowerCase());

        try {
            list = System.getProperty("HTTPClient.cookies.hosts.reject");
        } catch (Exception e) {
            list = null;
        }
        domains = Util.splitProperty(list);
        for (int idx = 0; idx < domains.length; idx++)
            addRejectDomain(domains[idx].toLowerCase());
    }

    /**
     * returns whether this cookie should be accepted. First checks the stored
     * lists of accept and reject domains, and if it is neither accepted nor
     * rejected by these then query the user via a popup.
     *
     * @param cookie
     *         the cookie in question
     * @param req
     *         the request
     * @param resp
     *         the response
     * @return true if we accept this cookie.
     */
    public boolean acceptCookie(Cookie cookie, RoRequest req, RoResponse resp) {
        String server = req.getConnection().getHost();
        if (server.indexOf('.') == -1)
            server += ".local";

        // Check lists. Reject takes priority over accept

        for (int idx = 0; idx < reject_domains.length; idx++) {
            if (reject_domains[idx].length() == 0 || reject_domains[idx].charAt(0) == '.'
                                                     && server.endsWith(reject_domains[idx]) || reject_domains[idx].charAt(0) != '.'
                                                                                                && server.equals(reject_domains[idx]))
                return false;
        }

        for (int idx = 0; idx < accept_domains.length; idx++) {
            if (accept_domains[idx].length() == 0 || accept_domains[idx].charAt(0) == '.'
                                                     && server.endsWith(accept_domains[idx]) || accept_domains[idx].charAt(0) != '.'
                                                                                                && server.equals(accept_domains[idx]))
                return true;
        }

        // Ok, not in any list, so ask the user (if allowed).

        if (!req.allowUI())
            return true;

        if (popup == null)
            popup = new BasicCookieBox();

        return popup.accept(cookie, this, server);
    }

    /**
     * This handler just allows all cookies to be sent which were accepted (i.e.
     * no further restrictions are placed on the sending of cookies).
     *
     * @return true
     */
    public boolean sendCookie(Cookie cookie, RoRequest req) {
        return true;
    }

    void addAcceptDomain(String domain) {
        if (domain.indexOf('.') == -1 && domain.length() > 0)
            domain += ".local";

        for (int idx = 0; idx < accept_domains.length; idx++) {
            if (domain.endsWith(accept_domains[idx]))
                return;
            if (accept_domains[idx].endsWith(domain)) {
                accept_domains[idx] = domain;
                return;
            }
        }
        accept_domains = Util.resizeArray(accept_domains, accept_domains.length + 1);
        accept_domains[accept_domains.length - 1] = domain;
    }

    void addRejectDomain(String domain) {
        if (domain.indexOf('.') == -1 && domain.length() > 0)
            domain += ".local";

        for (int idx = 0; idx < reject_domains.length; idx++) {
            if (domain.endsWith(reject_domains[idx]))
                return;
            if (reject_domains[idx].endsWith(domain)) {
                reject_domains[idx] = domain;
                return;
            }
        }

        reject_domains = Util.resizeArray(reject_domains, reject_domains.length + 1);
        reject_domains[reject_domains.length - 1] = domain;
    }
}

/**
 * A simple popup that asks whether the cookie should be accepted or rejected,
 * or if cookies from whole domains should be silently accepted or rejected.
 *
 * @author Ronald Tschal�r
 * @version 0.3-3 06/05/2001
 */
class BasicCookieBox extends Frame {
    private final static String title = "Set Cookie Request";

    private Dimension screen;

    private GridBagConstraints constr;

    private Label name_value_label;

    private Label domain_value;

    private Label ports_label;

    private Label ports_value;

    private Label path_value;

    private Label expires_value;

    private Label discard_note;

    private Label secure_note;

    private Label c_url_note;

    private Panel left_panel;

    private Panel right_panel;

    private Label comment_label;

    private TextArea comment_value;

    private TextField domain;

    private Button default_focus;

    private boolean accept;

    private boolean accept_domain;

    /** Constructs the popup. */
    BasicCookieBox() {
        super(title);

        screen = getToolkit().getScreenSize();

        addNotify();
        addWindowListener(new Close());

        GridBagLayout layout;
        setLayout(layout = new GridBagLayout());
        constr = new GridBagConstraints();

        constr.gridwidth = GridBagConstraints.REMAINDER;
        constr.anchor = GridBagConstraints.WEST;
        add(new Label("The server would like to set the following cookie:"), constr);

        Panel p = new Panel();
        left_panel = new Panel();
        left_panel.setLayout(new GridLayout(4, 1));
        left_panel.add(new Label("Name=Value:"));
        left_panel.add(new Label("Domain:"));
        left_panel.add(new Label("Path:"));
        left_panel.add(new Label("Expires:"));
        ports_label = new Label("Ports:");
        p.add(left_panel);

        right_panel = new Panel();
        right_panel.setLayout(new GridLayout(4, 1));
        right_panel.add(name_value_label = new Label());
        right_panel.add(domain_value = new Label());
        right_panel.add(path_value = new Label());
        right_panel.add(expires_value = new Label());
        ports_value = new Label();
        p.add(right_panel);
        add(p, constr);
        secure_note = new Label("This cookie will only be sent over secure connections");
        discard_note = new Label("This cookie will be discarded at the end of the session");
        c_url_note = new Label("");
        comment_label = new Label("Comment:");
        comment_value = new TextArea("", 3, 45, TextArea.SCROLLBARS_VERTICAL_ONLY);
        comment_value.setEditable(false);

        add(new Panel(), constr);

        constr.gridwidth = 1;
        constr.anchor = GridBagConstraints.CENTER;
        constr.weightx = 1.0;
        add(default_focus = new Button("Accept"), constr);
        default_focus.addActionListener(new Accept());

        Button b;
        constr.gridwidth = GridBagConstraints.REMAINDER;
        add(b = new Button("Reject"), constr);
        b.addActionListener(new Reject());

        constr.weightx = 0.0;
        p = new Separator();
        constr.fill = GridBagConstraints.HORIZONTAL;
        add(p, constr);

        constr.fill = GridBagConstraints.NONE;
        constr.anchor = GridBagConstraints.WEST;
        add(new Label("Accept/Reject all cookies from a host or domain:"), constr);

        p = new Panel();
        p.add(new Label("Host/Domain:"));
        p.add(domain = new TextField(30));
        add(p, constr);

        add(new Label("domains are characterized by a leading dot (`.');"), constr);
        add(new Label("an empty string matches all hosts"), constr);

        constr.anchor = GridBagConstraints.CENTER;
        constr.gridwidth = 1;
        constr.weightx = 1.0;
        add(b = new Button("Accept All"), constr);
        b.addActionListener(new AcceptDomain());

        constr.gridwidth = GridBagConstraints.REMAINDER;
        add(b = new Button("Reject All"), constr);
        b.addActionListener(new RejectDomain());

        pack();

        constr.anchor = GridBagConstraints.WEST;
        constr.gridwidth = GridBagConstraints.REMAINDER;
    }

    public Dimension getMaximumSize() {
        return new Dimension(screen.width * 3 / 4, screen.height * 3 / 4);
    }

    /** our event handlers */
    private class Accept implements ActionListener {
        public void actionPerformed(ActionEvent ae) {
            accept = true;
            accept_domain = false;
            synchronized (BasicCookieBox.this) {
                BasicCookieBox.this.notifyAll();
            }
        }
    }

    private class Reject implements ActionListener {
        public void actionPerformed(ActionEvent ae) {
            accept = false;
            accept_domain = false;
            synchronized (BasicCookieBox.this) {
                BasicCookieBox.this.notifyAll();
            }
        }
    }

    private class AcceptDomain implements ActionListener {
        public void actionPerformed(ActionEvent ae) {
            accept = true;
            accept_domain = true;
            synchronized (BasicCookieBox.this) {
                BasicCookieBox.this.notifyAll();
            }
        }
    }

    private class RejectDomain implements ActionListener {
        public void actionPerformed(ActionEvent ae) {
            accept = false;
            accept_domain = true;
            synchronized (BasicCookieBox.this) {
                BasicCookieBox.this.notifyAll();
            }
        }
    }

    private class Close extends WindowAdapter {
        public void windowClosing(WindowEvent we) {
            new Reject().actionPerformed(null);
        }
    }

    /**
     * the method called by the DefaultCookiePolicyHandler.
     *
     * @return true if the cookie should be accepted
     */
    public synchronized boolean accept(Cookie cookie, DefaultCookiePolicyHandler h, String server) {
        // set the new values

        name_value_label.setText(cookie.getName() + "=" + cookie.getValue());
        domain_value.setText(cookie.getDomain());
        path_value.setText(cookie.getPath());
        if (cookie.expires() == null)
            expires_value.setText("never");
        else
            expires_value.setText(cookie.expires().toString());
        int pos = 2;
        if (cookie.isSecure())
            add(secure_note, constr, pos++);
        if (cookie.discard())
            add(discard_note, constr, pos++);

        if (cookie instanceof Cookie2) {
            Cookie2 cookie2 = (Cookie2)cookie;

            // set ports list
            if (cookie2.getPorts() != null) {
                ((GridLayout)left_panel.getLayout()).setRows(5);
                left_panel.add(ports_label, 2);
                ((GridLayout)right_panel.getLayout()).setRows(5);
                int[] ports = cookie2.getPorts();
                StringBuffer plist = new StringBuffer();
                plist.append(ports[0]);
                for (int idx = 1; idx < ports.length; idx++) {
                    plist.append(", ");
                    plist.append(ports[idx]);
                }
                ports_value.setText(plist.toString());
                right_panel.add(ports_value, 2);
            }

            // set comment url
            if (cookie2.getCommentURL() != null) {
                c_url_note.setText("For more info on this cookie see: " + cookie2.getCommentURL());
                add(c_url_note, constr, pos++);
            }

            // set comment
            if (cookie2.getComment() != null) {
                comment_value.setText(cookie2.getComment());
                add(comment_label, constr, pos++);
                add(comment_value, constr, pos++);
            }
        }

        // invalidate all labels, so that new values are displayed correctly

        name_value_label.invalidate();
        domain_value.invalidate();
        ports_value.invalidate();
        path_value.invalidate();
        expires_value.invalidate();
        left_panel.invalidate();
        right_panel.invalidate();
        secure_note.invalidate();
        discard_note.invalidate();
        c_url_note.invalidate();
        comment_value.invalidate();
        invalidate();

        // set default domain test

        domain.setText(cookie.getDomain());

        // display

        setResizable(true);
        pack();
        setResizable(false);
        setLocation((screen.width - getPreferredSize().width) / 2,
                    (int)((screen.height - getPreferredSize().height) / 2 * .7));
        setVisible(true);
        default_focus.requestFocus();

        // wait for user input

        try {
            wait();
        } catch (InterruptedException e) {
        }

        setVisible(false);

        // reset popup

        remove(secure_note);
        remove(discard_note);
        left_panel.remove(ports_label);
        ((GridLayout)left_panel.getLayout()).setRows(4);
        right_panel.remove(ports_value);
        ((GridLayout)right_panel.getLayout()).setRows(4);
        remove(c_url_note);
        remove(comment_label);
        remove(comment_value);

        // handle accept/reject domain buttons

        if (accept_domain) {
            String dom = domain.getText().trim().toLowerCase();

            if (accept)
                h.addAcceptDomain(dom);
            else
                h.addRejectDomain(dom);
        }

        return accept;
    }
}

/** A simple separator element. */
class Separator extends Panel {
    public void paint(Graphics g) {
        int w = getSize().width, h = getSize().height / 2;

        g.setColor(Color.darkGray);
        g.drawLine(2, h - 1, w - 2, h - 1);
        g.setColor(Color.white);
        g.drawLine(2, h, w - 2, h);
    }

    public Dimension getMinimumSize() {
        return new Dimension(4, 2);
    }
}
